/* eslint-disable */
import MutationSummary from './MutationSummary';

export default class TreeMirrorClient {

  constructor(target, mirror, testingQueries) {
    this.target = target;
    this.mirror = mirror;
    this.nextId = 1;
    this.knownNodes = new MutationSummary.NodeMap();
    
    const rootId = this.serializeNode(target).id;
    const children = [];
    for (let child = target.firstChild; child; child = child.nextSibling) {
      children.push(this.serializeNode(child, true));
    }
    this.mirror.initialize(rootId, children);
    
    let queries = [{ all: true }];
    if (testingQueries) {
      queries = queries.concat(testingQueries);
    }
    this.mutationSummary = new MutationSummary({
      rootNode: target,
      callback: (summaries,ts) => {
        this.applyChanged(summaries,ts);
      },
      queries: queries
    });
  }

  disconnect() {
    if (this.mutationSummary) {
      this.mutationSummary.disconnect();
      this.mutationSummary = undefined;
    }
  }

  rememberNode(node) {
    const id = this.nextId++;
    this.knownNodes.set(node, id);
    return id;
  }

  forgetNode(node) {
    this.knownNodes.delete(node);
  }

  serializeNode(node, recursive) {
    if (node === null) {
      return null;
    }

    const id = this.knownNodes.get(node);
    if (id !== undefined) {
      return { id: id };
    }
    
    const data = {
      nodeType: node.nodeType,
      id: this.rememberNode(node)
    };
    
    switch (data.nodeType) {
      case Node.DOCUMENT_TYPE_NODE:
        const docType = node;
        data.name = docType.name;
        data.publicId = docType.publicId;
        data.systemId = docType.systemId;
        break;
      case Node.COMMENT_NODE:
      case Node.TEXT_NODE:
        data.textContent = node.textContent;
        break;
      case Node.ELEMENT_NODE:
        const elm = node;
        data.tagName = elm.tagName;
        data.attributes = {};
        for (let i = 0; i < elm.attributes.length; i++) {
          const attr = elm.attributes[i];
          data.attributes[attr.name] = attr.value;
        }
        if (recursive && elm.childNodes.length) {
          data.childNodes = [];
          for (let child = elm.firstChild; child; child = child.nextSibling)
            data.childNodes.push(this.serializeNode(child, true));
        }
        break;
    }

    return data;
  }

  serializeAddedAndMoved(added, reparented, reordered) {
    const all = added.concat(reparented).concat(reordered);
    const parentMap = new MutationSummary.NodeMap();

    all.forEach((node) => {
      const parent = node.parentNode;
      let children = parentMap.get(parent);
      if (!children) {
        children = new MutationSummary.NodeMap();
        parentMap.set(parent, children);
      }
      children.set(node, true);
    });
    const moved = [];
    parentMap.keys().forEach((parent) => {
      const children = parentMap.get(parent);
      let keys = children.keys();
      while (keys.length) {
        let node = keys[0];
        while (node.previousSibling && children.has(node.previousSibling)) {
          node = node.previousSibling;
        }

        while (node && children.has(node)) {
          const data = this.serializeNode(node);
          data.previousSibling = this.serializeNode(node.previousSibling);
          data.parentNode = this.serializeNode(node.parentNode);
          moved.push(data);
          children.delete(node);
          node = node.nextSibling;
        }

        keys = children.keys();
      }
    });
    return moved;
  }

  serializeAttributeChanges(attributeChanged) {
    const map = new MutationSummary.NodeMap();
    Object.keys(attributeChanged).forEach((attrName) => {
      attributeChanged[attrName].forEach((element) => {
        let record = map.get(element);
        if (!record) {
          record = this.serializeNode(element);
          record.attributes = {};
          map.set(element, record);
        }
        record.attributes[attrName] = element.getAttribute(attrName);
      });
    });
    return map.keys().map((node) => {
      return map.get(node);
    });
  }

  applyChanged(summaries,ts) {
    const summary = summaries[0];
    const removed = summary.removed.map((node) => {
      return this.serializeNode(node);
    });
    const moved = this.serializeAddedAndMoved(summary.added, summary.reparented, summary.reordered);
    const attributes = this.serializeAttributeChanges(summary.attributeChanged);
    const text = summary.characterDataChanged.map((node) => {
      const data = this.serializeNode(node);
      data.textContent = node.textContent;
      return data;
    });
    this.mirror.applyChanged(removed, moved, attributes, text,ts);
    summary.removed.forEach((node) => {
      this.forgetNode(node);
    });
  }
}

